#include "client.h"
#include "messages.h"
#ifdef USE_FMOD
   #include "soundsystemfmod.h"
#else
   #include "soundsystem.h"
#endif
#include <QtNetwork>
#include <QMessageBox>

#define RENDER_RATE_IN_MS 20 // 10=100hz; 100=10hz;

Client::Client(QObject *parent) : QObject(parent)
{
   mIsJoined = false;
   mPlayerID = 0;

   mTcpSocket = new QTcpSocket(this);
   connect(mTcpSocket, SIGNAL(readyRead()), this, SLOT(onData()));
   connect(mTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
           this, SLOT(onDisplayError(QAbstractSocket::SocketError)));
   mBlockSize = 0;

   game = new ClientGame();
   mRender = 0;

   mRenderTimer = new QTimer(this);
   connect(mRenderTimer, SIGNAL(timeout()), this, SLOT(onRenderTimer()));
   mRenderTimer->setInterval(RENDER_RATE_IN_MS);
   mRenderTimer->start();

#ifdef USE_FMOD
   SoundSystemFMOD::create();
#else
   SoundSystem::instance();
#endif

   qDebug() << "Client::Started";
}


Client::~Client()
{
   qDebug() << "~Client";

   SoundSystem::destroy();

   if (mRenderTimer)
   {
      mRenderTimer->stop();
      delete mRenderTimer;
   }
   if (mRender)
      delete mRender;
   if (mTcpSocket)
      delete mTcpSocket;
   if (game)
      delete game;
}


void Client::setRender(Render *render)
{
   mRender = render;
   mRender->setGame(game);
}


void Client::onRenderTimer()
{
   if (mRender && game)
   {
      mRender->tick();

   }
}


void Client::sendPlayerJoin(const QString &address, int port)
{
   mHostAddress = QHostAddress(address);
   mHostPort = port;
   mClientPort = port+1;
   mIsJoined = false;
   QHostAddress addr(address);

   mTcpSocket->connectToHost(address, port);
}


void Client::sendPlayerReady(const bool ready)
{
   if (mIsJoined)
   {
      QByteArray data;
      QDataStream ds(&data, QIODevice::WriteOnly);
      ds << (quint16)0
         << (qint32)NET_MSG_USER_EVENT
         << (qint32)EVENT_USER_READY
         << (quint32)mPlayerID
         << (bool)ready;
      // Put msg size at the front of msg
      ds.device()->seek(0);
      ds <<(quint16)(data.size() - sizeof(quint16));

      mTcpSocket->write(data);
      qDebug() << "Client::sendPlayerReady:" << ready;
   }
}

void Client::sendEntitySquadIDRequest()
{
   if (mIsJoined)
   {
      QByteArray data;
      QDataStream ds(&data, QIODevice::WriteOnly);
      ds << (quint16)0
         << (qint32)NET_MSG_USER_EVENT
         << (qint32)EVENT_ENTITY_SQUAD_ID_REQ
         << (quint32)mPlayerID;
      // Put msg size at the front of msg
      ds.device()->seek(0);
      ds <<(quint16)(data.size() - sizeof(quint16));

      mTcpSocket->write(data);
      qDebug() << "Client::sendSquadIDRequest:";
   }
}


void Client::sendEntitySquadAddToEmitter(const EntitySquadType &squad, const quint32 emitterID)
{
   if (mIsJoined)
   {

      QByteArray data;
      QDataStream ds(&data, QIODevice::WriteOnly);
      ds << (quint16)0
         << (qint32)NET_MSG_USER_EVENT
         << (qint32)EVENT_ENTITY_SQUAD_ADD_TO_EMITTER
         << (quint32)mPlayerID
         << (quint32)emitterID
         << (quint32)squad.id
         << (quint32)squad.size
         << (qint32)squad.stats.type
         << (qint32)squad.stats.kind
         << (qint32)squad.stats.size
         << squad.stats.color;

      // Put msg size at the front of msg
      ds.device()->seek(0);
      ds <<(quint16)(data.size() - sizeof(quint16));

      mTcpSocket->write(data);

      qDebug() << "Client::sendEntitySquadAddToEmitter: pid=" << mPlayerID << "squadID=" << squad.id << "emitterID=" <<  emitterID;
   }
}


void Client::sendEntitySquadAdd(const EntitySquadType &squad, const QPointF &position)
{
   if (mIsJoined)
   {
      QByteArray data;
      QDataStream ds(&data, QIODevice::WriteOnly);
      ds << (quint16)0
         << (qint32)NET_MSG_USER_EVENT
         << (qint32)EVENT_ENTITY_SQUAD_ADD
         << (quint32)mPlayerID
         << (qint32)squad.stats.type
         << (quint32)squad.id
         << (quint32)squad.size
         << (qint32)squad.stats.kind
         << (qint32)squad.stats.size
         << squad.stats.color
         << (qreal)position.x()
         << (qreal)position.y();

      // Put msg size at the front of msg
      ds.device()->seek(0);
      ds <<(quint16)(data.size() - sizeof(quint16));

      mTcpSocket->write(data);
      qDebug() << "Client::sendEntityAddSquad: pid=" << mPlayerID << "squadSize=" << squad.size << "pos=" << position;
   }
}


void Client::sendEntityAdd(const EntityBaseStats &stats, const QPointF &position)
{
   qDebug() << "Client::sendEntityAdd";
   if (mIsJoined)
   {
      QByteArray data;
      QDataStream ds(&data, QIODevice::WriteOnly);
      ds << (quint16)0
         << (qint32)NET_MSG_USER_EVENT
         << (qint32)EVENT_ENTITY_ADD
         << (quint32)mPlayerID
         << (qint32)stats.type
         << (qint32)stats.kind
         << (qint32)stats.size
         << stats.color
         << (qreal)position.x()
         << (qreal)position.y();
      // Put msg size at the front of msg
      ds.device()->seek(0);
      ds <<(quint16)(data.size() - sizeof(quint16));

      mTcpSocket->write(data);
      qDebug() << "Client::sendEntityAdd: pid=" << mPlayerID << "type=" << stats.type << "pos=" << position;
   }
}


void Client::onData()
{
   //qDebug() << "Client::onData from:" << mTcpSocket->peerAddress().toString();

   QDataStream data(mTcpSocket);
   //data.setVersion(QDataStream::Qt_4_0);

   while (mTcpSocket->bytesAvailable())
   {
      // Try and grab the msg size first, then we'll wait for
      // the whole msg...
      if (mBlockSize == 0)
      {
         if (mTcpSocket->bytesAvailable() < (int)sizeof(quint16))
         {
            //qDebug() << "Client::onData:waiting for blocksize";
            return;
         }
         data >> mBlockSize;
      }

      // Still don't have all the bytes... wait for more
      if (mTcpSocket->bytesAvailable() < mBlockSize)
      {
         //qDebug() << "Client::onData:waiting for more data";
         return;
      }

      //if (mTcpSocket->peerAddress()== mHostAddress) //needed?
      //{
      qint32 msgType = 0;
      data >> msgType;
      //qDebug() << "Client::onData: msgType=" << msgType;
      switch (msgType)
      {
      case NET_MSG_JOIN: processJoin(data); break;
      case NET_MSG_LEAVE: processLeave(data); break;
      case NET_MSG_GAME_EVENT: processGameEvent(data); break;
      default: break;
      }
      //}

      // Reset block size
      mBlockSize = 0;
   }
}


void Client::processJoin(QDataStream &data)
{
   data >> mIsJoined >> mPlayerID;
   qDebug() << "Client::processJoin:" << mIsJoined << "pid=" << mPlayerID;
}


void Client::processLeave(QDataStream &data)
{
   // Not used yet.
   //mIsJoined = false;
   //qDebug() << "Client::processLeave: " << mIsJoined;
}


void Client::processGameEvent(QDataStream &data)
{
   qint32 msgGameEventType = 0;
   data >> msgGameEventType;
   //qDebug() << "Client::processGameEvent type=" << msgGameEventType;
   switch(msgGameEventType)
   {
   case EVENT_ENTITY_UPDATES: processEntityUpdates(data); break;
   case EVENT_ENTITY_ADD: processEntityAdd(data); break;
   case EVENT_ENTITY_SQUAD_ADD_TO_EMITTER: processEntitySquadAddToEmitter(data); break;
   case EVENT_ENTITY_SQUAD_REMOVE_FROM_EMITTER: processEntitySquadRemoveFromEmitter(data); break;
   case EVENT_ENTITY_SQUAD_ID_REQ: processEntitySquadIDRequest(data); break;
   case EVENT_ENTITY_SQUAD_REMOVE: processEntitySquadRemove(data); break;
   case EVENT_ENTITY_ATTACK: processEntityAttack(data); break;
   case EVENT_ENTITY_REMOVE: processEntityRemove(data); break;
   case EVENT_GAME_MAPCHANGE: processMapChange(data); break;
   default: break;
   }
}


void Client::processMapChange(QDataStream &data)
{
   QString mapFile;
   data >> mapFile;
   qDebug() << "Client::processMapChange" << mapFile;

   if (game)
   {
      game->mapChange(mapFile);
      if (mRender)
         mRender->drawPaths(game->paths());
   }
}


void Client::processEntityAdd(QDataStream &data)
{
   //qDebug() << "Client::processEntityAdd";
   ClientEntity entity;
   qreal posX=0.0,posY=0.0;
   qint32 type=0;
   qreal emitX=0.0,emitY=0.0;

   data >> type  // FIXME - after this we can polymorph demarshalling
        >> entity.id >> entity.playerID >> entity.squadID >> entity.base.kind >> entity.base.size >> entity.base.color >> posX >> posY >> entity.rotation;

   switch (type)
   {
   case ENTITY_UNITEMITTER:
      data >> entity.emitterCapacity >> entity.emitterRate >> emitX >> emitY >> entity.emitterPath;
      entity.emitterPos = QPointF(emitX, emitY);
      //qDebug() << "Client::processEntityAdd emitter: cap=" << entity.emitterCapacity << "rate=" << entity.emitterRate << "pos=" << entity.emitterPos << "path=" << entity.emitterPath;
      break;
   default: break;
   }

   entity.base.type = (ENTITY_TYPE)type;
   entity.pos.setX(posX);
   entity.pos.setY(posY);
   entity.health = entity.base.size*10; // calculate health from size

   qDebug() << "Client::processEntityAdd id=" << entity.id << "squadID=" << entity.squadID << "type=" << entity.base.type << "kind=" << entity.base.kind << "size=" << entity.base.size << "color=" << entity.base.color << "pos=" << entity.pos.toPointF() << "rot=" << entity.rotation;

   ClientEntity *e = game->addEntity(entity);
   mRender->addEntity(e);
}


void Client::processEntitySquadAddToEmitter(QDataStream &data)
{
   quint32 emitterID;
   EntitySquadType squad;
   squad.stats.type = ENTITY_UNIT;

   data >> emitterID
        >> squad.id >> squad.size
        >> squad.stats.kind >> squad.stats.size >> squad.stats.color;

   qDebug() << "Client::processEntitySquadAddToEmitter emitter=" << emitterID << "squad=" << squad.id;

   ClientEntity *emitter = game->entities.value(emitterID);
   if (emitter)
      emitter->emitterContents.append(squad);
}


void Client::processEntitySquadRemoveFromEmitter(QDataStream &data)
{
   quint32 emitterID=0, squadID=0;

   data >> emitterID >> squadID;

   qDebug() << "Client::processEntitySquadRemoveFromEmitter emitter=" << emitterID << "squad=" << squadID;

   ClientEntity *emitter = game->entities.value(emitterID);
   if (emitter)
      emitter->emitterContents.removeFirst();
}


void Client::processEntityAttack(QDataStream &data)
{
   quint32 attacker=0, target=0;
   qint32 damage=0;
   data >> attacker >> target >> damage;
   qDebug() << "Client::processEntityAttack id=" << attacker << "targ=" << target << "dmg=" << damage;
   if (attacker!=0)
   {
      ClientEntity *atk = game->entities[attacker];
      if (atk)
      {
         if (target!=0)
         {
            ClientEntity *targ = game->entities[target];
            if (targ)
            {
               atk->target = targ;
               targ->health -= damage;

               switch (atk->base.kind)
               {
               case TOWER_BEAM:
                  mRender->addFX(FX_TOWER_BEAM_FIRE, atk);
                  mRender->addFX(FX_DAMAGE_TEXT, atk, damage);
                  break;
               default:
                  break;
               }
            }
         }
         else
            game->entities[attacker]->target = 0;
      }
   }
}


void Client::processEntitySquadRemove(QDataStream &data)
{
   quint32 squadID=0;
   data >> squadID;
   qDebug() << "Client::processEntitySquadRemove id=" << squadID ;
   emit squadRemoved(squadID);
}

void Client::processEntityRemove(QDataStream &data)
{
   quint32 id=0;
   data >> id;
   qDebug() << "Client::processEntityRemove id=" << id ;

   if (id!=0)
   {
      ClientEntity *e = game->entities.value(id);
      if (e)
      {
         mRender->addFX(FX_UNIT_DEATH, e);

         game->entities.remove(id);
         mRender->removeEntity(id);

         foreach (ClientEntity *entity, game->entities)
         {
            if (entity->target == e)
               entity->target = 0;
         }

         delete e;
      }
   }
}


void Client::processEntityUpdates(QDataStream &data)
{
   //qDebug() << "Client::processEntityUpdates";
   qint32 numEntities=0;
   quint32 id=0;
   qreal posX=0.0, posY=0.0, velX=0.0, velY=0.0, rot=0;

   data >> numEntities;

   for (int i=0; i<numEntities; i++)
   {
      data >> id >> posX >> posY >> velX >> velY >> rot;

      if (game->entities.contains(id))
      {
         ClientEntity *entity = game->entities[id];
         entity->pos.setX(posX);
         entity->pos.setY(posY);
         entity->vel.setX(velX);
         entity->vel.setY(velY);
         entity->rotation=rot;
      }
      else // Add entity if it doesn't exist locally
      {
         qDebug() << "ERROR: Client::processEntityUpdates: No entity info for id=" << id;
      }
   }
}


void Client::processEntitySquadIDRequest(QDataStream &data)
{
   quint32 id;
   data >> id;
   qDebug() << "Client::processEntitySquadIDRequest:" << "id=" << id;
   emit newSquadID(id);
}

void Client::onDisplayError(QAbstractSocket::SocketError socketError)
{
   switch (socketError)
   {
   case QAbstractSocket::RemoteHostClosedError:
   break;
   case QAbstractSocket::HostNotFoundError:
      qDebug() << "Client::The host was not found."; break;
   case QAbstractSocket::ConnectionRefusedError:
      qDebug() << "Client::The connection was refused by the peer."; break;
   default:
      qDebug() <<  tr("Client::The following error occurred: %1.").arg(mTcpSocket->errorString()); break;
   }
}
